package org.wggds.webservices.io;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.wggds.webservices.io.data.CompleteInformation;
import org.wggds.webservices.io.data.CompositionComponent;
import org.wggds.webservices.io.data.ListOperation;
import org.wggds.webservices.io.data.OutputFormat;
import org.wggds.webservices.io.data.QueryResult;
import org.wggds.webservices.io.query.CompositionSearchQuery;
import org.wggds.webservices.io.query.IdListQuery;
import org.wggds.webservices.io.query.ListOperationQuery;
import org.wggds.webservices.io.query.MassSearchQuery;
import org.wggds.webservices.io.query.SubstructureQuery;
import org.wggds.webservices.io.query.WggdsQuery;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

public class WggdsQueryOutputUtil 
{
	public static String writeQuery(WggdsQuery query) throws WggdsQueryExcpetion, UnknownWggdsQuerySubClass{
		if(query instanceof SubstructureQuery){
			return writeSubstructureQuery((SubstructureQuery) query);
		}else if(query instanceof MassSearchQuery){
			return writeMassSearchQuery((MassSearchQuery)query);
		}else if(query instanceof CompositionSearchQuery){
			return writeCompositionSearchQuery((CompositionSearchQuery) query);
		}else if(query instanceof IdListQuery){
			return writeIdListQuery((IdListQuery)query);
		}else if(query instanceof ListOperationQuery){
			return writeListOperationQuery((ListOperationQuery) query);
		}else{
			throw new UnknownWggdsQuerySubClass();
		}
	}
	
	public static List<QueryResult> runQuery(WggdsQuery query, String uri) throws WggdsQueryExcpetion, UnknownWggdsQuerySubClass{
		String message=writeQuery(query);
		message=URLEncoder.encode(message);
		message="query="+message;
		
		HttpURLConnection connection=null;
		try{
			URL url=new URL(uri+query.getName());
			connection=(HttpURLConnection) url.openConnection();
			connection.setRequestMethod("POST");
			connection.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
		    connection.setRequestProperty("Content-Length", "" + Integer.toString(message.getBytes().length));
	        connection.setRequestProperty("Content-Language", "en-US");  
				
	        connection.setUseCaches (false);
	        connection.setDoInput(true);
	        connection.setDoOutput(true);
	        
	        DataOutputStream stream=new DataOutputStream(connection.getOutputStream());
	        stream.write(message.getBytes());
	        stream.flush();
	        stream.close();
	        
	        BufferedReader reader=new BufferedReader(new InputStreamReader(connection.getInputStream()));
	        StringBuffer buf=new StringBuffer();
	        String line;
	        while((line=reader.readLine())!=null){
	        	buf.append(line+"\n");
	        }
	        
	        return WggdsQueryOutputUtil.parseQueryResult(buf.toString()); 
		} catch (MalformedURLException e) {
			throw new WggdsQueryExcpetion("Error sending query to remote server", e);
		} catch (IOException e) {
			throw new WggdsQueryExcpetion("Error sending query to remote server", e);
		}finally{
			if(connection!=null){
				connection.disconnect();
			}
		}
	}
	
	
    /**
     * <?xml version="1.0" encoding="UTF-8"?>
     * <query completeInformation="false" reducingEnd="true" terminal="false" exactMatch="true" otherResidues="true"
     *          ignoreReducingAlditol="false" format="xml">
     *   <sequence>
     *   ...
     *   </sequence>
     * </query>
     */
    public static String writeSubstructureQuery(SubstructureQuery a_query) throws WggdsQueryExcpetion
    {
        Element t_root = new Element("query");
        WggdsQueryOutputUtil.setCompleteInformation(t_root,"completeInformation",a_query.isCompleteInformation());
        WggdsQueryOutputUtil.setBoolean(t_root,"reducingEnd",a_query.isReducingEnd());
        WggdsQueryOutputUtil.setBoolean(t_root,"terminal",a_query.isTerminal());
        WggdsQueryOutputUtil.setBoolean(t_root,"exactMatch",a_query.isExactMatch());
        WggdsQueryOutputUtil.setBoolean(t_root,"otherResidues",a_query.isOtherResidues());
        WggdsQueryOutputUtil.setBoolean(t_root,"ignoreReducingAlditol",a_query.isIgnoreReducingAlditol());
        WggdsQueryOutputUtil.setFormat(t_root,a_query.getFormat());
        Element t_glyde = new Element("sequence");
        t_glyde.setText(a_query.getSequence());
        t_root.addContent(t_glyde);
        return WggdsQueryOutputUtil.writeXML(t_root);
    }

    private static void setFormat(Element a_root, OutputFormat a_outputFormat) throws WggdsQueryExcpetion
    {
        if ( a_outputFormat == null )
        {
            throw new WggdsQueryExcpetion("Output format is not set.");
        }
        a_root.setAttribute("format",a_outputFormat.getFormat());
    }

    private static void setBoolean(Element a_root, String a_name,
            boolean a_value)
    {
        if ( a_value )
        {
            a_root.setAttribute(a_name,"true");
        }
        else
        {
            a_root.setAttribute(a_name,"false");
        }
    }

    private static String writeXML(Element a_root) throws WggdsQueryExcpetion
    {
        Document t_document = new Document(a_root);
        Format t_objFormat = Format.getPrettyFormat();
        XMLOutputter t_exportXML = new XMLOutputter(t_objFormat);
        StringWriter t_writer = new StringWriter();
        try
        {
            t_exportXML.output(t_document, t_writer );
        } 
        catch (IOException t_exception)
        {
            throw new WggdsQueryExcpetion(t_exception.getMessage(),t_exception);
        }
        return t_writer.toString();
    }

    /**
     * <?xml version="1.0" encoding="UTF-8"?>
     * <query completeInformation="false" mass="960.2" interval="300.0" ppm="true" persubstitution="none" monoisotopic="true" format="xml"/>
     */
    public static String writeMassSearchQuery(MassSearchQuery a_query) throws WggdsQueryExcpetion
    {
        Element t_root = new Element("query");
        WggdsQueryOutputUtil.setCompleteInformation(t_root,"completeInformation",a_query.isCompleteInformation());
        WggdsQueryOutputUtil.setBoolean(t_root,"ppm",a_query.isIntervalPPM());
        WggdsQueryOutputUtil.setBoolean(t_root,"monoisotopic",a_query.isMonoisotopic());
        WggdsQueryOutputUtil.setFormat(t_root,a_query.getFormat());
        WggdsQueryOutputUtil.setDouble(t_root,"mass",a_query.getMass());
        WggdsQueryOutputUtil.setDouble(t_root,"interval",a_query.getInterval());
        if ( a_query.getPersubstitution() == null )
        {
            throw new WggdsQueryExcpetion("Missing persubstition value.");
        }
        t_root.setAttribute("persubstitution",a_query.getPersubstitution().getName());
        return WggdsQueryOutputUtil.writeXML(t_root);
    }

    private static void setDouble(Element a_root, String a_name,
            Double a_double) throws WggdsQueryExcpetion
    {
        if ( a_double == null )
        {
            throw new WggdsQueryExcpetion("Missing value for " + a_name + ".");
        }
        a_root.setAttribute(a_name,a_double.toString());
    }

    /**
     * <?xml version="1.0"?>
     * <query completeInformation="true" allowOtherResidues="true" format="xml">
     *    <component minValue="0" maxValue="2">
     *        <![CDATA[...]]>
     *    </component>
     *    <component minValue="0" maxValue="2">
     *        <![CDATA[...]]>
     *    </component>
     * </query> 
     */
    public static String writeCompositionSearchQuery(CompositionSearchQuery a_query) throws WggdsQueryExcpetion
    {
        Element t_root = new Element("query");
        WggdsQueryOutputUtil.setCompleteInformation(t_root,"completeInformation",a_query.isCompleteInformation());
        WggdsQueryOutputUtil.setBoolean(t_root,"allowOtherResidues",a_query.isAllowOtherResidues());
        WggdsQueryOutputUtil.setFormat(t_root,a_query.getFormat());
        if ( a_query.getComponents().size() < 1 )
        {
            throw new WggdsQueryExcpetion("Missing components.");
        }
        for (CompositionComponent t_compontent : a_query.getComponents())
        {
            t_root.addContent(WggdsQueryOutputUtil.createComponent(t_compontent));
        }
        return WggdsQueryOutputUtil.writeXML(t_root);
    }

    /**
     * <component minValue="0" maxValue="2">
     *        <![CDATA[...]]>
     * </component>
     */
    private static Element createComponent(CompositionComponent a_component) throws WggdsQueryExcpetion
    {
        Element t_root = new Element("component");
        if ( a_component.getMinValue() != null )
        {
            t_root.setAttribute("minValue",a_component.getMinValue().toString());
        }
        if ( a_component.getMaxValue() != null )
        {
            t_root.setAttribute("maxValue",a_component.getMaxValue().toString());
        }
        t_root.setText(a_component.getResidue());
        return t_root;
    }

    /**
     * <?xml version="1.0" encoding="UTF-8"?>
     * <query completeInformation="false" listOne="2010.07.29_A23DFC56V" listTwo="2010.07.27_C56VQ23ER" operation="AND" format="xml"/>
     * @throws WggdsQueryExcpetion 
     */
    public static String writeListOperationQuery(ListOperationQuery a_query) throws WggdsQueryExcpetion
    {
        Element t_root = new Element("query");
        WggdsQueryOutputUtil.setCompleteInformation(t_root,"completeInformation",a_query.isCompleteInformation());
        WggdsQueryOutputUtil.setFormat(t_root,a_query.getFormat());
        if ( a_query.getOperation() == null )
        {
            throw new WggdsQueryExcpetion("Missing operation value.");
        }
        t_root.setAttribute("operation",a_query.getOperation().getOperation());
        if ( a_query.getListOne() == null )
        {
            throw new WggdsQueryExcpetion("Missing list one.");
        }
        t_root.setAttribute("listOne",a_query.getListOne());
        if ( a_query.getOperation() != ListOperation.NOT )
        {
            if ( a_query.getListTwo() == null )
            {
                throw new WggdsQueryExcpetion("Missing list two.");
            }
            t_root.setAttribute("listTwo",a_query.getListTwo());
        }
        return WggdsQueryOutputUtil.writeXML(t_root);
    }

    /**
     * @param a_root
     * @param a_string
     * @param a_completeInformation
     */
    private static void setCompleteInformation(Element a_root, String a_name,
            CompleteInformation a_completeInformation)
    {
        a_root.setAttribute(a_name,a_completeInformation.getName());
    }

    /**
     * <?xml version="1.0" encoding="UTF-8"?>
     * <query namespace="namespace_of_the_database" glydeVersion="1.0" format="xml">
     *      <structure id="carbNxxx"/>
     *      <structure id="carbNxxx"/>
     *      <structure id="carbNxxx"/>
     * </query>
     * @throws WggdsQueryExcpetion 
     */
    public static String writeIdListQuery(IdListQuery a_query) throws WggdsQueryExcpetion
    {
        Element t_root = new Element("query");
        WggdsQueryOutputUtil.setFormat(t_root,a_query.getFormat());
        if ( a_query.getNamespace() == null )
        {
            throw new WggdsQueryExcpetion("Missing namespace value.");
        }
        t_root.setAttribute("namespace",a_query.getNamespace());
        if ( a_query.getGlydeVersion() == null )
        {
            throw new WggdsQueryExcpetion("Missing Glyde version.");
        }
        t_root.setAttribute("glydeVersion",a_query.getGlydeVersion());

        for (String t_id : a_query.getIdList())
        {
            Element t_structure = new Element("structure");
            t_structure.setAttribute("id",t_id);
            t_root.addContent(t_structure);
        }
        return WggdsQueryOutputUtil.writeXML(t_root);
    }
    
    public static List<QueryResult> parseQueryResult(String xmlString) throws WggdsQueryExcpetion{
    	try{
    		DocumentBuilderFactory factory=DocumentBuilderFactory.newInstance();
    		factory.setNamespaceAware(true);
    		DocumentBuilder builder=factory.newDocumentBuilder();

    		org.w3c.dom.Document doc=builder.parse(new InputSource(new StringReader(xmlString)));

    		XPathFactory xPathFactory=XPathFactory.newInstance();
    		XPath xpath=xPathFactory.newXPath();

    		XPathExpression expr=xpath.compile("//structure");
    		XPathExpression findSeqElements=xpath.compile("sequence");

    		NodeList nodeList=(NodeList)expr.evaluate(doc, XPathConstants.NODESET);

    		List<QueryResult> queryResults=new ArrayList<QueryResult>();

    		for(int i=0;i<nodeList.getLength();i++){
    			Node node=nodeList.item(i);

    			QueryResult queryResult=new QueryResult();

    			queryResult.setUrl(getNodeValueOrNull(node, "url"));
    			queryResult.setId(getNodeValueOrNull(node, "id"));

    			Node seqNode=(Node)findSeqElements.evaluate(node,XPathConstants.NODE);

    			if(seqNode!=null){
    				queryResult.setSequence(seqNode.getTextContent());
    			}

    			queryResults.add(queryResult);
    		}

    		return queryResults;
    	}catch(IOException e){
    		throw new WggdsQueryExcpetion("Error processing query result XML",e);
    	} catch (ParserConfigurationException e) {
    		throw new WggdsQueryExcpetion("Error processing query result XML",e);
		} catch (SAXException e) {
			throw new WggdsQueryExcpetion("Error processing query result XML",e);
		} catch (XPathExpressionException e) {
			throw new WggdsQueryExcpetion("Error processing query result XML",e);
		}
    }
    
    private static String getNodeValueOrNull(Node node, String attributeName){
    	if(node.getAttributes().getNamedItem(attributeName)==null){
    		return null;
    	}else{
    		return node.getAttributes().getNamedItem(attributeName).getNodeValue();
    	}
    }
}
